﻿using System.IO;
using System.Text;

namespace DatabaseGroup.SSIS.SourceAdapters.Parser
{
    /// <summary>
    /// Defines common file operations
    /// </summary>
    internal interface IFileReader
    {
        char GetNextChar();
        bool IsEOF { get; }
    }

    /// <summary>
    /// Reads data from a file
    /// </summary>
    internal class FileReader : IFileReader
    {
        private const int _EOF = -1;
        private const int _BufferSize = 65536;

        private bool _endOfFile;
        private FileStream _stream;
        private readonly BinaryReader _reader;

        /// <summary>
        /// Initializes a new instance of the FileReader class 
        /// using the filename and encoding
        /// </summary>
        /// <param name="fileName">The file to open</param>
        /// <param name="encoding">The character encoding to use</param>
        public FileReader(string fileName, Encoding encoding)
        {
            ArgumentVerifier.CheckObjectArgument(encoding, "encoding");

            OpenStream(fileName);
            _reader = new BinaryReader(_stream, encoding);

            SkipBOM();
        }

        /// <summary>
        /// Closes the underlying file stream
        /// </summary>
        public void Close()
        {
            _reader.Close();
            _stream.Close();
        }

        /// <summary>
        /// Opens a new FileStream to a file
        /// </summary>
        /// <param name="fileName">The file to open</param>
        private void OpenStream(string fileName)
        {
            ArgumentVerifier.CheckFileNameArgument(fileName);

            _stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, _BufferSize);
        }

        #region IFileReader Members

        /// <summary>
        /// Reads the next character in the file
        /// </summary>
        /// <returns>The character read</returns>
        public char GetNextChar()
        {
            int retChar = this._reader.Read();
            if (retChar == _EOF)
            {
                _endOfFile = true;
            }

            return (char)retChar;
        }

        /// <summary>
        /// Gets if the end of the file has been reached
        /// </summary>
        public bool IsEOF
        {
            get
            {
                return _endOfFile;
            }
        }

        /// <summary>
        /// Processes the first 4 initial bytes of a file used to determine character encoding
        /// </summary>
        /// <param name="byteBuffer">The first bytes of the file</param>
        /// <returns>The number of bytes used to specify the character encoding</returns>
        internal static int CountBOMBytes(byte[] byteBuffer)
        {
            if (byteBuffer.Length < 2)
                return 0;

            if (byteBuffer[0] == 0xFE && byteBuffer[1] == 0xFF)
            {
                // Big Endian Unicode
                return 2;
            }
            if (byteBuffer[0] == 0xFF && byteBuffer[1] == 0xFE)
            {
                // Little Endian Unicode (or possibly little endian UTF32)
                // TODO: handle little endian UTF32 properly as this should return 4
                return 2;
            }

            if (byteBuffer.Length < 3)
                return 0;

            if (byteBuffer[0] == 0xEF && byteBuffer[1] == 0xBB && byteBuffer[2] == 0xBF)
            {
                // UTF-8
                return 3;
            }

            if (byteBuffer.Length < 4)
                return 0;

            if (byteBuffer[0] == 0 && byteBuffer[1] == 0 && byteBuffer[2] == 0xFE && byteBuffer[3] == 0xFF)
            {
                // Big Endian UTF32
                return 4;
            }

            // Unknown - rewind back to the start.
            return 0;
        }

        /// <summary>
        /// Skips past any initial bytes in the file used to specify the file character encoding
        /// </summary>
        private void SkipBOM()
        {
            byte[] byteBuffer = _reader.ReadBytes(4);

            int count = CountBOMBytes(byteBuffer);

            // Move the file position if less than 4 bytes are used.
            if (count != 4)
            {
                _reader.BaseStream.Seek(count, SeekOrigin.Begin);
            }
        }

        #endregion
    }
}
